Skip to content

rpc: implement eth_capabilities method#20951

Open
lupin012 wants to merge 27 commits into
mainfrom
lupin012/impl_eth_capabilities
Open

rpc: implement eth_capabilities method#20951
lupin012 wants to merge 27 commits into
mainfrom
lupin012/impl_eth_capabilities

Conversation

@lupin012
Copy link
Copy Markdown
Contributor

@lupin012 lupin012 commented May 2, 2026

Implements eth_capabilities per ethereum/execution-apis#755.

Returns per-category data availability (state, tx, logs, receipts, blocks, stateproofs) with oldestBlock computed from the node's prune mode:

  • archive: all fields from block 0
  • full: state/logs/receipts from head-pruneDistance, tx/blocks from 0
  • minimal: all fields from head-pruneDistance
  • stateproofs: disabled unless --prune.include-commitment-history is set

Also caches the commitment-history-enabled flag (written once at startup) in BaseAPI to avoid a DB read per call

Closes #19762

lupin012 and others added 8 commits May 2, 2026 10:07
Implements eth_capabilities per ethereum/execution-apis#755.

Returns per-category data availability (state, tx, logs, receipts, blocks,
stateproofs) with oldestBlock computed from the node's prune mode:
- archive: all fields from block 0
- full: state/logs/receipts from head-pruneDistance, tx/blocks from 0
- minimal: all fields from head-pruneDistance
- stateproofs: disabled unless --prune.include-commitment-history is set

Also caches the commitment-history-enabled flag (written once at startup)
in BaseAPI and Generator to avoid a DB read per call, and migrates
eth_receipts.go and eth_simulation.go to use the new cached helper.

Closes #19762

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the cached atomic.Pointer[bool] field from Generator and the
internal rawdb.ReadDBCommitmentHistoryEnabled call inside GetReceipts.
Callers with BaseAPI access use the existing atomic cache via
api.commitmentHistoryEnabled(tx); other callers read from the DB directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lupin012 lupin012 added the RPC label May 11, 2026
@lupin012 lupin012 marked this pull request as ready for review May 13, 2026 10:03
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Implements eth_capabilities per execution-apis #755. Also refactors commitmentHistoryEnabled reads: the flag is lifted into a single param threaded through Generator.GetReceipts, with a cached accessor on BaseAPI. Signature change is propagated through all call sites.

Verified locally: builds cleanly, TestCapabilities passes.

Major — spec compliance

The spec at #755 has evolved since the linked URL; I pulled the current schema and the canonical fixture (commit 9cb5230). The shape in the spec example:

{
  "head": {"number": "0x...", "hash": "0x..."},
  "state": {"disabled": false, "oldestBlock": "0x...", "deleteStrategy": {"type": "window", "retentionBlocks": 90000}},
  ...
  "blocks": {"disabled": false, "oldestBlock": "0x0"},   // archive — no deleteStrategy
  "stateproofs": {"disabled": true}                       // disabled — nothing else
}
  1. head is missing. The schema (src/schemas/capabilities.yaml) lists head as required alongside state, tx, logs, receipts, blocks, stateproofs. Needs to be added — {number: hexutil.Uint64, hash: common.Hash} populated from the canonical head.
  2. deleteStrategy is missing. Optional per schema, but it's the field that lets routers distinguish "kept forever" (omitted) from "sliding window of N blocks" ({type: \"window\", retentionBlocks: N}). Without it a router can't tell archive from full from minimal. Map prune.Distance distances to retentionBlocks; omit deleteStrategy for DefaultBlocksPruneMode / KeepAllBlocksPruneMode.

Major — incorrect tx/blocks for chains with history expiry

rpc/jsonrpc/eth_system.go:93:

blocksOldest := pruneMode.Blocks.PruneTo(headBlock)

For prune.FullMode, Blocks == DefaultBlocksPruneMode (= math.MaxUint64), so PruneTo returns 0 — and the PR advertises tx.oldestBlock = 0 and blocks.oldestBlock = 0. But DefaultBlocksPruneMode actually means "use chain-specific history expiry," and the existing consumers handle the merge cutoff explicitly:

  • db/snapshotsync/snapshotsync.go:264 for receipts: if pruneMode.Blocks == prune.DefaultBlocksPruneMode && cc.MergeHeight != nil { pruneHeight = *cc.MergeHeight }
  • db/snapshotsync/snapshotsync.go:244 isTransactionsSegmentExpired: pre-merge transaction segments are never downloaded under DefaultBlocksPruneMode.

So on mainnet / sepolia / gnosis / bloatnet (all have mergeBlock in their chainspecs) running --prune.mode=full, eth_getTransactionByHash for a pre-merge tx will fail, but eth_capabilities will advertise tx.oldestBlock = 0x0. The advertised range and the actually-served range disagree — exactly what this API is designed to prevent.

Suggested fix (and add a test that uses a chainspec with MergeHeight set):

blocksOldest := pruneMode.Blocks.PruneTo(headBlock)
if pruneMode.Blocks == prune.DefaultBlocksPruneMode && chainConfig.MergeHeight != nil {
    blocksOldest = *chainConfig.MergeHeight
}

You'll need chainConfig in Capabilities; api.chainConfig(ctx, tx) returns it.

Minor

  • AnswerGetReceiptsQuery reads the flag even on full cache hits (p2p/protocols/eth/handlers.go:375). Previously the read was inside Generator.GetReceipts after the receiptsCache.Get short-circuit, so cache hits avoided the DB. Now ReadDBCommitmentHistoryEnabled(db) runs unconditionally before the loop. Single key lookup per query (not per block), so impact is small.
  • BaseAPI.commitmentHistoryEnabled doesn't cache false from an absent key. Intentional per the comment, but pruneMode (mirrored pattern) caches whatever prune.Get returns even when keys are empty. Fine in production (key is always written at startup by checkAndSetCommitmentHistoryFlag), but worth a one-line acknowledgment that this can briefly hit the DB per request during the boot window.
  • Misleading comment at eth_system.go:90: "PruneTo returns 0 when the distance is MaxUint64 (archive/full-blocks)". PruneTo returns 0 whenever distance > stageHead, not specifically for MaxUint64. Also archive and full-blocks use different sentinels (KeepAllBlocksPruneMode = MaxUint64-1 vs DefaultBlocksPruneMode = MaxUint64) — which is exactly the distinction the history-expiry issue turns on.
  • Test coverage gap: TestCapabilities uses chain.TestChainBerlinConfig (no MergeHeight), so the history-expiry branch isn't exercised. Add a sub-test with a chainspec that sets MergeHeight once the fix above lands.

Other notes

  • Spec is still OPEN (not merged). Two member approvals so far, but the schema can still move — the recent comment thread already added head, renamed trienodesstateproofs, made oldestBlock optional, and removed deleteStrategy: none. Worth keeping a close eye on it before merging the Erigon side.
  • RPC_VERSION bump from v2.9.0v2.10.0: presumes a matching tag exists in erigontech/rpc-tests containing fixtures for eth_capabilities. Worth confirming that tag is published, otherwise QA RPC tests will fail to fetch.

Risk summary

Risk
Correctness Medtx/blocks wrong for full mode on chains with MergeHeight
Spec compliance Med — missing required head; missing deleteStrategy
Performance Low — one extra DB read per p2p receipts query
API stability Low — spec PR may still change before merge

Recommendation

Add head (required), add deleteStrategy, and fix the history-expiry case for tx/blocks (with a chainspec-with-MergeHeight test). The rest is solid — interface threading is clean, tests cover the prune-mode matrix, caching pattern mirrors existing code well.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for the eth_capabilities RPC method and refactors receipt-generation call paths to avoid repeated DB reads for the commitment-history flag.

Changes:

  • Implement eth_capabilities and add JSON response types for per-category availability ranges.
  • Cache the commitment-history-enabled flag in BaseAPI and thread it into receipt generation.
  • Update receipt getter interfaces/callers (JSON-RPC, P2P, tests) for the new GetReceipts signature.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
rpc/jsonrpc/receipts/receipts_generator.go Passes commitment-history flag into receipt generation instead of reading it internally.
rpc/jsonrpc/receipts/handler_test.go Updates receipt generator usage to new GetReceipts signature.
rpc/jsonrpc/eth_system.go Implements eth_capabilities and defines response structs.
rpc/jsonrpc/eth_system_test.go Adds test coverage for eth_capabilities across prune modes and commitment-history flag.
rpc/jsonrpc/eth_simulation.go Uses cached commitmentHistoryEnabled accessor instead of direct DB read.
rpc/jsonrpc/eth_receipts.go Uses cached commitmentHistoryEnabled accessor and passes it into receipt generation.
rpc/jsonrpc/eth_api.go Extends EthAPI interface; adds cached commitmentHistoryEnabled to BaseAPI.
p2p/protocols/eth/handlers.go Updates ReceiptsGetter interface and threads commitment-history flag once per request.
p2p/protocols/eth/handlers_test.go Updates mock ReceiptsGetter to match new interface.
execution/tests/blockchain_test.go Updates receipt reading helper to pass commitment-history flag.
execution/abi/bind/backends/simulated.go Updates simulated backend receipt path to pass commitment-history flag.
.github/workflows/scripts/rpc_version.env Bumps RPC API version to v2.10.0.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rpc/jsonrpc/eth_system.go
Comment thread rpc/jsonrpc/eth_system.go Outdated
Comment thread rpc/jsonrpc/eth_system.go
Comment thread rpc/jsonrpc/eth_system_test.go Outdated
lupin012 and others added 8 commits May 13, 2026 20:11
The execution-apis spec (PR #755) marks head as required alongside
the data-category fields. Populate it with the canonical chain tip
(number + hash) at call time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Height

On chains with MergeHeight set (mainnet, sepolia, gnosis) running full
prune mode, DefaultBlocksPruneMode causes PruneTo to return 0, but
pre-merge block/tx segments are never downloaded. Advertise MergeHeight
as the oldest available block instead of 0.

Adds a full_merge_height sub-test to TestCapabilities to cover this path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old comment "MaxUint64 = keeps all block snapshots" was wrong:
DefaultBlocksPruneMode uses chain-specific history expiry and does not
preserve pre-merge blocks on merge chains.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ReadDBCommitmentHistoryEnabled was called unconditionally before the
fetch loop, paying a DB round-trip even when the entire query was
already cached (pendingIndex == len(query)). Guard it behind a
pendingIndex < len(query) check so cache hits avoid the read.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unlike pruneMode, false is intentionally not cached when the DB key is
absent: during the boot window before checkAndSetCommitmentHistoryFlag
runs, caching false would shadow a subsequent true write.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional deleteStrategy to CapabilityField: when a category uses
a finite prune window (Distance), set {type:"window", retentionBlocks:N}
so routers can distinguish archive/full/minimal modes. Omit the field
for KeepAllBlocksPruneMode and DefaultBlocksPruneMode which do not use
a fixed retention window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw "window" string with a named constant and compute
avail(stateOldest, history) once for state/logs/receipts instead
of three identical calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ify comments

When --persist.receipts is enabled, receipts are stored from genesis so
receipts.oldestBlock should be 0, not the state history prune window.
Check kvcfg.PersistReceipts.Enabled and report accordingly.

Also clarify two misleading comments:
- PruneTo comment now distinguishes KeepAllBlocksPruneMode (keep all)
  from DefaultBlocksPruneMode (chain-specific history expiry).
- Test comment no longer implies DefaultBlocksPruneMode keeps all
  block snapshots unconditionally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lupin012 lupin012 requested a review from lystopad as a code owner May 14, 2026 18:07
@lupin012 lupin012 requested a review from yperbasis May 17, 2026 05:40
@lupin012
Copy link
Copy Markdown
Contributor Author

@yperbasis All the points from your review have been addressed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread rpc/jsonrpc/eth_system.go Outdated
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few items I'd like to see addressed:

Correctness / spec conformance

1. logs ignores --persist.receipts. Logs is always pinned to stateField, but when persistReceipts is on, logs (which are stored as part of each receipt) are effectively available from genesis too. Clients reading eth_capabilities will skip queries the node can actually answer. At minimum Logs should match Receipts when persistReceipts is on.

2. CapabilityField.Disabled always serializes as \"disabled\": false. The spec marks disabled as optional (present only when true). Suggest:

Disabled bool `json:\"disabled,omitempty\"`

Otherwise the v2.10.1 fixtures may well fail on the disabled-absence assertions; even if they don't today, it's safer aligned with the spec.

3. JSON field name stateproofs (lowercase). Worth double-checking against the latest revision of execution-apis#755 — earlier drafts used stateProofs (camelCase). If the spec lands on camelCase this needs to change before clients integrate.

4. receipts.oldestBlock seam on merge chains in full mode (without --persist.receipts). Receipts re-execution requires both state history and the block to be present. The code returns stateField, which on a merge chain in full mode is head − pruneDistance. In steady state on mainnet this is fine (head − pruneDistance ≫ MergeHeight), but right at/after the merge it would over-promise. Either compute max(stateOldest, blocksOldest) or document the assumption.

Documentation gap

5. Missing entry in cmd/rpcdaemon/README.md. Erigon's canonical RPC method matrix lives there (the eth_* block around lines 254–326). New methods are conventionally added with an EIP/spec annotation in the notes column (precedent: eth_configEIP-7910 on line 262, eth_getBlockAccessListAdded in Amsterdam (EIP-7928) on line 272). Please slot eth_capabilities in alongside eth_config with a link to execution-apis#755.

Tests

6. Coverage gaps worth filling. The mode × commitment matrix is good, but I'd like:

  • minimal_persist_receipts — proves Receipts.oldestBlock = 0 overrides the prune window across all modes (not just full).
  • full_merge_height with pruneDistance > head − MergeHeight — exercises the receipts/logs seam from item 4 above.
  • head = 0 boundary — pins that ReadCanonicalHash(tx, 0) returns the genesis hash, not zero.

Plumbing nit (non-blocking)

7. Boolean threaded through ReceiptsGetter.GetReceipts. Every caller now has to remember the flag. Caching inside Generator itself (atomic bool, set on first successful read) would give the same per-process savings without widening the interface or touching simulated.go/blockchain_test.go/p2p. Not a blocker — but it would have been a smaller diff. If you keep the parameter, please at least consider an opts struct so future flags don't keep widening the signature.

Minor

  • The asymmetry between commitmentHistoryEnabled() (doesn't cache on `!ok`) and pruneMode() (caches default) is sensible but slightly surprising; a one-line comment in pruneMode() pointing at the boot-window rationale would help the next reader.
  • The persistReceiptsOpts ...bool variadic in setupAPI for a single optional flag reads awkwardly — a named bool would be clearer.

Overall the implementation is solid and the cache plumbing is a nice incidental win; items 1 and 2 are the ones I'd consider blocking, the rest are smaller asks.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Comment thread rpc/jsonrpc/eth_system.go Outdated
Comment thread rpc/jsonrpc/eth_system.go Outdated
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the round-trip — the receipts seam, README entry, full_merge_height/head_zero tests, and persist-receipts handling are all in good shape. One blocker I created, plus a couple of follow-ups.

Blocker — omitempty on Disabled (my prior advice was wrong, mea culpa)

I asked for Disabled bool + "" + json:"disabled,omitempty" + "" + in the last round, claiming the spec marked it optional. That was incorrect — the spec schema (src/schemas/capabilities.yaml, commit 9cb5230d) lists disabledunderrequired`:

EthCapabilitiesEffectiveResource:
  required:
    - disabled
  properties:
    disabled:
      type: boolean

And the canonical fixture in execution-apis#755 has "disabled": false present in every non-disabled category. The Erigon QA fixture in erigontech/rpc-tests v2.10.1 (integration/mainnet/eth_capabilities/test_01.json) mirrors that:

"state":       { "disabled": false, "oldestBlock": "0x0" },
"tx":          { "disabled": false, "oldestBlock": "0x0" },
...
"stateproofs": { "disabled": true }

The May 19 mainnet-rpc-integ-tests run on this branch (26121305203) failed on this exact test across http/http_comp/websocket — eth_capabilities/test_01.json failed: diff mismatch. Reproduce: the commit 1445da50 flipped json:"disabled"json:"disabled,omitempty", and Go's encoder drops false fields with omitempty, so the daemon now emits "state":{"oldestBlock":"0x0"} while the fixture expects "state":{"disabled":false,"oldestBlock":"0x0"}.

Fix is one character — revert Disabled to json:"disabled", keep the ,omitempty on OldestBlock/DeleteStrategy so disabled-only categories still serialize as {"disabled":true}.

Aside: the fixture's metadata.ignoreFields: ["result.stateproofs", "result.head"] is documentation only — the v2.10.1 runner (integration/run_tests.py + src/rpctests/integration/compare.py) doesn't honor it. So result.head will also mismatch the static 0xd4e56740... once disabled is fixed. Either set the expected result to null (which should_ignore_diff does honor as a wildcard via expected_from_file), or have the fixture omit the dynamic fields. Worth a follow-up on the rpc-tests side; otherwise this test will start failing the moment the disabled fix lands.

Medium — logsField = stateField under-restricts when blocks are pruned tighter than state

The full_merge_height_receipts_seam test pins this behavior:

require.Equal(t, mergeAt, oldest(t, result.Receipts))   // 18 (correct — needs block body)
require.Equal(t, stateOldest, oldest(t, result.Logs))   // 10 (under-restricts)

In Erigon (rpc/jsonrpc/eth_receipts.go:399-407), getLogsV3 looks up matches via LogTopicIdx/LogAddrIdx (state-history-bound), then calls _txnReader.TxnByIdxInBlock to fetch the txn for receipt re-generation. When block bodies are pruned in [stateOldest, blocksOldest), TxnByIdxInBlock returns nil and the loop continues — the index matches are silently dropped. So advertising logs.oldestBlock = stateOldest means clients querying that window get partial / incomplete logs without any error.

In mainnet steady-state this doesn't matter (head − pruneDistance ≫ MergeHeight, so the binding constraint is state and the two fields converge). It only bites right after the merge or in the test config. Still, this is exactly the routing-correctness contract eth_capabilities exists to enforce — recommend logsField = receiptsField in the non-persist branch, and update the test comment ("logs only require state history") which is what made me sign off last round.

Minor

  • persistReceiptsOpts ...bool in setupAPI. I flagged this last round; still reads awkwardly for a single optional flag. A named persistReceipts bool parameter would be clearer (and removes the len(persistReceiptsOpts) > 0 && persistReceiptsOpts[0] guard).
  • Duplication in full_merge_height / full_merge_height_receipts_seam. ~30 lines of identical chain-with-merge-height setup. A helper setupAPIWithMergeHeight(t, mergeAt, pruneMode, commitmentHistory) would compress both.
  • copier.CopyWithOption(..., DeepCopy: true) on chain.Config. Fine — the codebase explicitly uses jinzhu/copier for chain.Config (see the comment at execution/chain/chain_config.go:42: "Config must be copied only with jinzhu/copier since it contains a sync.Once.").

Risk summary

Risk
Correctness Low — logs seam edge case only relevant post-merge / in tests
Spec compliance High — current code emits invalid JSON per the schema, blocking the QA RPC test
API stability Low — execution-apis#755 still open but the disabled requirement has been stable across all revisions I checked back to March

Recommendation

Remove ,omitempty from Disabled, align logsField with receiptsField (with a comment about Erigon's getLogsV3 pruning behavior), update the seam-test assertion accordingly. Once disabled is fixed, separately address the head mismatch in the rpc-tests fixture (set the expected result to null, or omit the head field from the response object), otherwise the test will fail for a different reason. Apologies again for the round-trip — the v2.10.1 fixture failure is on me.

lupin012 and others added 3 commits May 20, 2026 11:15
- Fix Disabled field: remove omitempty so disabled:false is always present
  (spec marks it required; its absence caused QA fixture failures)
- Fix logs oldest-block: use receiptsField instead of stateField in the
  non-persist branch (getLogsV3 uses block bodies, not state history)
- Fix persist-receipts+MergeHeight over-reporting: when DefaultBlocksPruneMode
  applies on a merge chain, oldest = mergeHeight rather than 0
- Fix overlay inconsistency: use overlayTx for both GetLatestBlockNumber
  and ReadCanonicalHash so headBlock and headHash come from the same view
- Replace bare bool in ReceiptsGetter.GetReceipts with ReceiptsOpts struct
  to keep the interface stable as new options are added
- Reduce test duplication with setupAPIWithMerge helper; add
  full_persist_receipts_merge_height sub-test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread rpc/jsonrpc/eth_system.go
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — retentionBlocks serialized as hex string, spec wants plain integer

rpc/jsonrpc/eth_system.go:55-57:

type DeleteStrategy struct {
    Type            string         `json:"type"`
    RetentionBlocks hexutil.Uint64 `json:"retentionBlocks"`
}

hexutil.Uint64.MarshalText emits "0x..." (common/hexutil/json.go:118-124), so the daemon will return e.g. "retentionBlocks":"0x15f90". The spec schema (src/schemas/capabilities.yaml at commit 9cb5230d) declares:

retentionBlocks:
  type: integer
  minimum: 0

…and both canonical fixtures (get-capabilities.io, get-capabilities-disabled-stateproofs.io) show plain numbers like "retentionBlocks":90000, "retentionBlocks":2350000. Routers validating against the schema will reject the Erigon response, and oldestBlock right next to it — correctly hex-encoded because the schema $refs uint — makes the inconsistency more glaring.

Note that the schema convention here is deliberate: oldestBlock is a chain quantity (hex) but retentionBlocks is a configuration scalar (decimal integer). Mixing them under hexutil.Uint64 collapses that distinction.

Fix:

type DeleteStrategy struct {
    Type            string `json:"type"`
    RetentionBlocks uint64 `json:"retentionBlocks"`
}

…and drop the cast at the single call site in Capabilities (rb := hexutil.Uint64(d)rb := uint64(d)).

The reason this slipped through is that TestCapabilities only asserts on Go struct fields (f.DeleteStrategy.RetentionBlocks) and never round-trips through json.Marshal. Please add one wire-format assertion per major shape variant (window strategy, no strategy, disabled), e.g.:

raw, err := json.Marshal(result)
require.NoError(t, err)
require.Contains(t, string(raw), `"retentionBlocks":10`)             // not "0xa"
require.Contains(t, string(raw), `"oldestBlock":"0x`)                 // hex
require.Contains(t, string(raw), `"disabled":false`)                  // always present
require.Contains(t, string(raw), `"stateproofs":{"disabled":true}`)   // disabled shape

This pins the wire format independent of struct definitions — would have caught both this issue and the earlier omitempty regression in 1445da50, and protects future refactors of CapabilityField.

Minor — Tx and Blocks reported identically without explanation

Tx:     blocksField,
Blocks: blocksField,

The spec schema models these as independent axes (the canonical fixture has tx with a window retention while blocks is full archive — clients that maintain a separate tx-lookup index can populate both fields meaningfully). For Erigon the conflation is correct because tx-by-hash availability is tied to block body availability (no independent tx-lookup-index pruning), but a one-line comment above the return makes this a deliberate Erigon-specific choice rather than an apparent oversight:

// In Erigon, tx-by-hash lookups go through block bodies — no independent tx index
// pruning — so tx availability == blocks availability.
Tx:     blocksField,

Nit — redundant eth package alias

rpc/jsonrpc/eth_receipts.go:26, rpc/jsonrpc/receipts/receipts_generator.go:39, execution/abi/bind/backends/simulated.go:60, execution/tests/blockchain_test.go:

eth "github.com/erigontech/erigon/p2p/protocols/eth"

The package is already named eth (p2p/protocols/eth/*.go all declare package eth), so the leading eth is redundant — drop it.

@lupin012 lupin012 requested a review from yperbasis May 20, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eth: add eth_capabilities RPC method

3 participants